DeepSeek V4.1-Flash 已合入 main PR #16925 · commit 0deca3181 · 2026-09-21 合入 · 73 文件 +10554/-665 · 代码基线 a71b766ce
2026-09-10,DeepSeek 发布 DeepSeek-V4.1-Flash: 552B 参数 MoE(每 token 激活 prefill 8B / decode 16B),训练 64K 稀疏注意力、外推 1M 上下文。 它的主线思路是把 KV Cache 压到极限,靠三件新架构武器:
| 武器 | 一句话 | 关键数字 |
|---|---|---|
| CED 因果编码器-解码器 | 解码器的全局 KV 不由自己的 20 层逐层产生——输入先过 20 层因果编码器,再由末端隐状态一次性投影出解码器要用的全局 KV | 持久 KV ≈ V4-Flash 的 1/8 |
| CSA2 压缩稀疏注意力 2 | 每层静态分派 Full / Reindex / Reuse 三种模式,跨层共享主 KV 与索引 K、复用 Top-K 索引;主 KV 用 FP4 存 | 全局 KV 890 B/token |
| Engram 条件记忆 | 196B 参数的稀疏查表记忆:token 哈希命中才注入隐状态,不命中不花访存 | 按需访问,不全驻显存 |
三者相乘,得到 890 字节/token 的全局 KV + 1M 上下文。上游 vLLM main 已带 V4.1 模型实现 (vllm commit 84030bbe)—— 但昇腾一处都跑不动。
能二次成功的关键:把大特性拆成"算子先行 → 框架跟上"两步走,二次提交只做框架集成、对齐当周 main、重建回归测试。 这也是这个 PR 体积高达 73 文件的原因——框架层一次补齐。
诉求不是"能 import",而是教程里的完整部署形态:A3 双机 16 卡(DP4×TP8+EP)、 1M 上下文、block 128、W8A8 权重,能出 token、能开全量解码图、能跑 DSpark 投机 (教程)。 这决定了改动必然横跨模型、注意力、缓存、算子、Runner 五层——即"五件套":
| 件 | 载体 | 干什么 |
|---|---|---|
| 注意力执行边界 | attention/dsa_v41.py(1098 行) | 每层的三阶段执行:双流预处理 → 索引选择 → 双阶段稀疏注意力 |
| 模型与缓存 | models/deepseek_v41/(12 文件) | 拓扑层规划、MoE/视觉、四槽页分配、压缩环形缓冲 |
| 条件记忆 | models/deepseek_v41/engram/(7 文件) | Engram 的 HBM 存储 + CPU 主机卸载 + 跨 DP 副本共享 |
| 投机解码 | models/deepseek_v41/dspark.py + spec_decode | DSpark 草稿模型,复用目标侧共享 SWA KV |
| 平台接线 | patch/ + worker/model_runner_v1.py(+254 行) | 上游 config 注入、Engram 前向元数据、图输入定址 |
| ★ 改动 | 没有它会怎样 |
|---|---|
★ 新增 attention/dsa_v41.py | 注意力请求第一步就查无后端——图1 的第 1 个断点原样保留,服务起不来 |
★ 新增 models/deepseek_v41/(model.py 1647 行为主) | 上游 V4.1 权重在昇腾上没有可执行载体:拓扑规划、MoE、Engram 注入、视觉全无 |
★ 新增 models/deepseek_v41/cache_config.py(163 行) | KV、索引 K、FP16 scale、滑窗四类缓存各自为战争抢显存——四槽"同页打包、一个块 ID 定位"的布局无从谈起 |
engram/ 七文件:HostUvaBuffer 钉住内存(UVA,统一虚拟寻址:主机内存可被设备直接寻址)+ Triton INT8 融合查表 + 跨 DP 共享dspark.py(583 行):DSpark 草稿模型,_store_standard_swa_kv 复用共享滑窗缓存indexer.py(227 行):接入 QLI V3 算子(QLI = Quantized Lightning Indexer,量化闪电索引器:用 INT8 的 Q/K 给每个 token 挑出值得读的 Top-K 位置)attention/context_parallel/dsa_v41_cp.py(240 行):CP(Context Parallel,上下文并行)下复制缓存先行更新model_runner_v1.py:Engram 前向元数据、lookback 窗口、定长图输入地址patch/platform/ 三件:Engram / DSpark / KV 布局的 config 注入compressor.py)、QLI Triton 预热、ModelSlim W8A8 配置、9 个 UT 文件、484 行教程 roles.append(
DeepseekV41LayerRole(
layer_idx=layer_idx,
compress_ratio=ratio,
kv_source=..., # ↳ 该层全局 KV 来自哪个源层(CED:解码器 KV 由编码器源层投影)
index_source=..., # ↳ 该层稀疏索引来自哪个索引源层(跨层共享 TopK)
is_candidate_source=layer_idx == candidate_source, # ⚡分层索引器:候选池只由首个 Full 层构建
# Consumer layers inherit the selection policy of their index
# source. For example, layer 26 reuses layer 24 TopK, and that
# TopK was computed inside layer 20's candidate blocks. ← 原注释
uses_candidate_filter=index_source is not None and index_source > candidate_source,
engram_slot=engram_slots.get(layer_idx), # ↳ 该层是否挂 Engram 条件记忆注入
)
)
def get_layer_tuples(specs):
"""Return DSV4-style ordered layer tuples and their physical page sizes."""
mla = {name for name, spec in specs.items() if isinstance(spec, AscendMLAAttentionSpec)}
state = sorted((name for name, spec in specs.items() if isinstance(spec, CircularBufferSpec)), key=_layer_number)
swa = {name for name, spec in specs.items() if isinstance(spec, AscendSlidingWindowMLASpec)}
...
for slot_idx, kv_name in enumerate(full):
prefix = kv_name.rsplit(".", 1)[0]
index_name = prefix + ".indexer.k_cache" # ⚡索引器 K 缓存与全局 KV 同槽配对
index_spec = specs[index_name]
kv_spec = specs[kv_name]
aliases = ([state[slot_idx]] if slot_idx < len(state) else []) + target_swa[slot_idx :: len(full)]
kv_bytes = kv_spec.unpadded_page_size_bytes
index_bytes = index_spec.unpadded_page_size_bytes
...
capacity = max( # ⚡页容量=槽内最大成员:索引 K 与 FP16 scale 排在 KV 之后同页(压缩比2:64 行/131072B;压缩比1:128 行/147712B)
kv_bytes + index_bytes,
*(specs[name].unpadded_page_size_bytes for name in aliases),
)
layer_tuples.append((kv_name, index_name, *aliases)) # ⚡一个块 ID 同时定位四类缓存(层外置)
page_sizes.append(capacity)
return page_sizes, layer_tuples
_select_sparse_indices)和阶段③(npu_sparse_flash_mla),其余都是收尾。def forward(self, attn, positions, hidden_states, output: torch.Tensor | None = None):
# ↳ 自定义算子调用方传入图稳定输出缓冲:O 投影结果直写,免去图边界的整张拷贝(⚡昇腾图优化)
if output is None:
output = torch.empty_like(hidden_states)
forward_context = get_forward_context()
if forward_context.attn_metadata is None:
output.zero_() # ↳ dummy 前向(剖析/捕获期)直接清零返回
return output
metadata = self._get_layer_metadata(forward_context.attn_metadata)
self._prepare_inputs_and_caches(attn, hidden_states, metadata, forward_context.attn_metadata)
num_tokens = metadata.swa.num_actual_tokens
if num_tokens:
positions = metadata.positions[:num_tokens]
cos, sin = metadata.rope(attn.rotary_emb.layername, num_tokens)
q, qr = self._prepare_queries(attn, hidden_states, positions, cos, sin, metadata) # ⚡阶段①:双流 Q 投影∥KV Cube
compressed_indices = self._select_sparse_indices(attn, hidden_states, qr, positions, cos, sin, metadata) # ⚡阶段②:QLI V3 候选三模式
attention_output = self._forward_attention(attn, q, metadata, compressed_indices) # →NPU 阶段③:SparseFlashMla 双阶段
torch.ops._C_ascend.inplace_partial_rotary_mul( # →NPU:输出侧逆 RoPE 原地完成,不落临时张量
attention_output.unsqueeze(1),
cos,
-sin,
rotary_mode="interleave",
partial_slice=[attn.nope_head_dim, attn.head_dim],
)
else:
heads = attn.n_heads if getattr(attn, "enable_dsa_cp", False) else attn.n_local_heads
attention_output = hidden_states.new_empty((0, heads, attn.head_dim))
self._project_output(attn, attention_output, hidden_states, metadata, projected=output)
return output
| 跟进 PR | 改了什么 | 为什么 |
|---|---|---|
| #17116 | 只在 vLLM 0.29 以上的版本通道注册 V4.1 与其 DSpark | v0.29 缺上游 V4.1 config/model 模块,import 即失败,连非 V4.1 模型都被拖垮 |
| #17134 | Engram 主机头分片跨本地 DP 副本共享(SharedUvaBuffer) | 多副本各存一份主机侧权重,白白浪费内存 |
| #17243 | Engram 平台补丁加模块可用性守卫 | 无 vllm.config.engram 的上游构建在导入补丁时启动即崩(nightly 回归) |
部署形态:A3 双机 16 卡 DP4 × TP8 + EP、--max-model-len 1048576、--block-size 128,
tokenizer / reasoning-parser / tool-call-parser 三件套均为 deepseek_v41(教程)。
接力棒:到这里已经讲清"改了什么"。想知道"模型设计的红利还有哪些没兑现、下一步能优化什么", 请读续篇 专题:CED/CSA2 架构 × vllm-ascend 性能协助点(7 条)。